Skip to content

feat(resources): browsable room finder with availability and dropdown filters - #8264

Closed
Rikdekker wants to merge 4 commits into
nextcloud:mainfrom
Rikdekker:feature/visual-room-browser-v2
Closed

feat(resources): browsable room finder with availability and dropdown filters#8264
Rikdekker wants to merge 4 commits into
nextcloud:mainfrom
Rikdekker:feature/visual-room-browser-v2

Conversation

@Rikdekker

@Rikdekker Rikdekker commented May 5, 2026

Copy link
Copy Markdown
Contributor

Reworked on top of the design in #8718, against current main.

The "Show rooms" dialog listed every room in a flat table with a button per row
to check its availability. With more than a handful of rooms that is not
browsable: no way to narrow down by building, capacity or features, and no
indication of what is free.

The dialog now contains a room browser: rooms grouped per building, each card
showing capacity, room number and whether the room is free for the time range
of the event. Filters for building, floor, minimum capacity and features narrow
the list down. A room is picked by clicking its card; nothing is written to the
event until "Done" is pressed, so closing the dialog any other way discards the
choice.

Scope, as agreed in your comment

  • Only the contents of the modal changed. The quick search, the list of already
    selected rooms and resources, and the suggestions are untouched — the diff on
    ResourceList.vue is four lines added, five removed.
  • Rooms only. The Resources tab from the design is left for a follow-up.
  • "Unavailable" is the only status shown. Nothing was factored out of
    AvatarParticipationStatus.vue; feat(editor): check resource availability while editing #8589 stays as it is.

Structure

Search and filter logic is plain TypeScript in src/utils/roomFilter.ts,
wrapped in a useRoomFilter(allRooms) composable, so it is unit tested without
mounting a component. Types live in src/types/models/roomFilter.ts.

const { buildingOptions, selectedBuilding, minimumSeatingCapacity,
        roomsFiltered, groupedRooms, searchText, resetFilters } = useRoomFilter(allRooms)

Two things derived in the frontend on purpose

Both per your feedback on #8263:

  • Building name is the first non-postal-code segment of the building
    address. Rooms carry no building name, so this is a heuristic, deliberately
    kept out of the principal model. Skipping postal codes matters in practice: on
    our test instance 19 of 113 rooms have 1098 XG, Amsterdam as their entire
    address, which would otherwise group them under "1098 XG".
  • LOCATION is built here rather than taken from roomAddress, which joins
    room number, story and address in that order and returns an empty string
    rather than null when unset.

One deviation from the design

Each card keeps a small action that opens the existing free/busy timeline
("Find a time"). The design drops it, but it is the one thing today's dialog
does that the browser would otherwise lose. Happy to remove it if you would
rather keep step one strictly to the mockup.

roompicker rooms-popup select-room-availability

Naming

The dialog is titled "Rooms" rather than "Rooms and resources", to match the
rooms-only scope. When the Resources tab lands, the dialog title, the "Show
rooms" button and the "Resources" section heading above it should be aligned in
one go — that heading already covers both rooms and resources today.

Tests

48 unit tests across roomFilter, useRoomFilter and attendee. npm run ts:check reports the same 47 pre-existing errors as clean main — none in the
new files. eslint and stylelint clean.

Verified manually on a Nextcloud 34 instance with 113 rooms from a real import:
filtering, grouping, selection, Done, LOCATION, round-tripping the selection,
re-checking availability on a time change, and the free/busy timeline.

Split out of #7996 as suggested by @nimishavijay; the principal mapping it
depended on landed separately in #8693.

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/models/principal.js 80.95% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Rikdekker
Rikdekker force-pushed the feature/visual-room-browser-v2 branch from bd9b34d to 3edf2ae Compare May 5, 2026 20:46
@SebastianKrupinski

Copy link
Copy Markdown
Contributor

Hi @Rikdekker

Thank you for the PR. I will review it as soon as I can.

@github-actions

Copy link
Copy Markdown

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@osm-frasch

Copy link
Copy Markdown

Thank you for working on this improvement.

I work for a non-profit educational and social services provider in Germany. We are currently evaluating RoomVox in our managed Nextcloud Enterprise environment, and this browsable room finder would make room booking significantly more intuitive and useful for our staff.

As we cannot apply custom patches to the Calendar app in our Enterprise environment, we would highly appreciate seeing this functionality reviewed and, if accepted, included in an official Nextcloud Calendar release.

Many thanks for your work on this.

@osm-frasch

Copy link
Copy Markdown

Sorry – just a quick follow-up: has there been any progress on this matter?

@osm-frasch

Copy link
Copy Markdown

Sorry – just a quick follow-up: has there been any progress on this matter?
Sorry—nothing new?

Comment thread src/components/Editor/Resources/ResourceList.vue Outdated
Comment thread src/components/Editor/Resources/ResourceList.vue Outdated
@GVodyanov

Copy link
Copy Markdown
Contributor

Thank you for your PR, could you please use our CSS variables for styling https://docs.nextcloud.com/server/latest/developer_manual/html_css_design/css.html

@Rikdekker

Copy link
Copy Markdown
Contributor Author

Thanks for the review @GVodyanov! I've pushed bec668d0d addressing the CSS feedback:

  • All hardcoded pixel spacing in ResourceList.vue and ResourceRoomCard.vue now uses var(--default-grid-baseline) (e.g. gap: 4pxgap: var(--default-grid-baseline), padding: 6px 10pxcalc(var(--default-grid-baseline) * 1.5) calc(var(--default-grid-baseline) * 2.5), etc.).
  • Dropped the hardcoded hex colour fallbacks — status colours now use the bare --color-success-text / --color-error-text server variables.
  • Normalised the group-header chevrons to :size="20" to match the section MapMarker icon.

The only raw px values left are the 1–3px hairline/accent border widths, for which there's no grid variable and which the existing Calendar components also use as literals. Happy to convert those too if you'd prefer.

Comment thread src/components/Editor/Resources/ResourceList.vue
Comment on lines +116 to +160
}
interface RoomPrincipal {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All interfaces should be separate files, so they can be reused

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially done in 3ec5a62. During a full review, I will check what interfaces can be really reused and which are only internal to a component (e.g., only used internally or used to type the component interface (props, emits, etc.)).

Comment thread vitest.config.js
@hamza221 hamza221 moved this from 📄 To do to 🏗️ In progress in 💌 📅 👥 Groupware team Jul 30, 2026
@hamza221 hamza221 moved this from 🏗️ In progress to 📄 To do in 💌 📅 👥 Groupware team Jul 30, 2026
@odzhychko odzhychko moved this from 📄 To do to 🏗️ In progress in 💌 📅 👥 Groupware team Aug 3, 2026
@odzhychko
odzhychko marked this pull request as draft August 3, 2026 05:19
@odzhychko

Copy link
Copy Markdown
Contributor

Hi @Rikdekker, I took over reviewing this and the related PR.
Sorry for the inconvenience of adding the reviewers in the middle of a longstanding PR.
But I'm currently the one that can dedicate attention to this feature 😄

We very much like the improvement in browsing rooms.

I am currently collecting input on how we can integrate this feature in a way that aligns with the current Nextcloud design language and upcoming features in the editor interface.
You will hear back from me soon, hopefully with a very concrete mockup.

@nimishavijay nimishavijay mentioned this pull request Aug 11, 2026
10 tasks
@odzhychko

Copy link
Copy Markdown
Contributor

Hi @Rikdekker

We prepared the design #8718 as something, we would directly incorporate into Nextcloud.
Would you like to reconcile your logic with the new design and update this (or open a new PR)?

Some nice to haves, but not required, would be:

  • extract the logic to build filter-options (incl. translations) into own code (plain TypeScript)
  • extract the logic for search + application of filters into its own code (plain TypeScript)
  • create a composable to encapsulating search/filter options + state + results from the actual Vue component
    • e.g., const { buildingOptions, selectedBuilding, minmialSeatingCapacity, roomsFiltered, searchText, ... } = useRoomFilter(allRooms)

This all would make it easier test and maybe reusable in other places.

With regards to #8263 (comment) :

  • Lets use your original heuristic for building name here
  • You have good points with regards to the format in LOCATION
    • feel free to ignore roomAddress and use the logic you had to construct something sensible

Look out for:

  • In the first step lets only change the contents of the Modal
    • Keep the quick search and overview of already selected rooms and resources as is in the editor view
  • In the first step only have the "Rooms" part of the modal
    • "Resources" can be cleanly added in next steps (including data mapping, search logic, select handling, status etc.)
  • In the first step let's keep only showing the status to "Unavailable"
    • feat(editor): check resource availability while editing #8589 just added cool logic about checking on change and adding more useful info
      • But factoring that out from AvatarParticipationStatus.vue would be better left to a separate PR.
      • Also might it might needs some though because in the modal we select a room only after clicking "Done".

The "Show rooms" dialog listed every room in a flat table with a button
per row to check its availability. With more than a handful of rooms
that is not browsable: there is no way to narrow down by building,
capacity or features, and no indication of what is free.

Replace its contents with a room browser: rooms grouped per building,
each card showing capacity, room number and whether the room is free for
the time range of the event. Filters for building, floor, minimum
capacity and features narrow the list down. A room is picked by clicking
its card; nothing is written to the event until "Done" is pressed, so
closing the dialog any other way discards the choice.

The editor view itself is untouched: the quick search, the list of
already selected rooms and resources, and the suggestions all stay as
they are. Only the contents of the dialog changed.

Search and filter logic lives in plain TypeScript (utils/roomFilter.ts)
and is wrapped in a useRoomFilter composable, so it is unit tested
without mounting a component and can be reused elsewhere.

Two things are derived in the frontend on purpose, per review feedback
on nextcloud#8263:

- The building name is the first segment of the building address. Rooms
  carry no building name of their own, so this is a heuristic, kept out
  of the principal model.
- The LOCATION value is built here rather than taken from the dav
  principal, whose roomAddress joins room number, story and address in
  that order and returns an empty string rather than null when unset.

Per-card access to the free/busy timeline is kept, so the availability
detail the old dialog offered is not lost.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Rikdekker <Rikdekker@users.noreply.github.com>
Labels sat inside the text fields but above the selects, so the filter
row lined up on neither the labels nor the field boxes, and the controls
had different heights. Give every filter the same shape: a label above
its control, in a grid that reflows by available width. Matches the
design in nextcloud#8718, which labels its filters the same way and leaves the
search field to its placeholder.

Also collapse the search input of a closed select. vue-select keeps it
next to the selected value, where it has nothing to type into and only
leaves a stray caret; it regains its width when the dropdown opens.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Rikdekker <Rikdekker@users.noreply.github.com>
Two problems with real imported room data, where 19 of 113 rooms have
"1098 XG, Amsterdam" as their entire building address:

The building name was the first address segment, which for those rooms
is the postal code, so the room browser showed a group headed "1098 XG".
Skip postal codes when deriving the name: a room with a building keeps
its building, one without degrades to the street, and one with neither
degrades to the city.

The LOCATION value pulled the first segment out as the building and put
the rest in front of it. With the same data that produced
"Amsterdam (Amsterdam, Room 0.01)". Keep the address in the order the
backend published it and append the room number, so nothing has to be
guessed about what each segment means.

Also stop the filter row overflowing the dialog: NcSelect defaults to a
min-width of 260px, which does not fit four filters side by side.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Rikdekker <Rikdekker@users.noreply.github.com>
The filter row lined up on neither the labels nor the field boxes: a
text field puts its label inside the border, a select puts it above.
Give every filter the same shape — a label above its control — and lay
them out two per row, which is what fits: a select keeps a min-width of
260px, so four side by side ran past the edge of the dialog. Follows the
filter layout of the design in nextcloud#8718.

Also join the meta line of a card into one string. It was assembled from
separate parts with a generated separator, which ended up in the
accessibility tree as a stray character for every card of a collapsed
building.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Rikdekker <Rikdekker@users.noreply.github.com>
@Rikdekker
Rikdekker force-pushed the feature/visual-room-browser-v2 branch from 590067c to 9fe285d Compare August 13, 2026 08:50
@Rikdekker
Rikdekker marked this pull request as ready for review August 13, 2026 08:58
@Rikdekker

Copy link
Copy Markdown
Contributor Author

@odzhychko Reworked on the new design and force-pushed. Description updated.

All five "look out for" points are in: modal contents only, rooms only,
"Unavailable" only, editor view untouched, AvatarParticipationStatus.vue not
touched. The three nice-to-haves as well — filter options and search/filter
logic are plain TypeScript, wrapped in useRoomFilter(allRooms).

Note the branch was rebuilt on current main rather than rebased, so your three
fixups from 3 August are no longer in its history — #8693 landed the model layer
they were correcting, and the UI they touched was rewritten for the new design.

Two things worth your opinion:

  1. I kept a per-card action that opens the existing free/busy timeline. It is
    not in the design, but it is the only thing the current dialog does that
    would otherwise disappear. Say the word and it goes.
  2. Building name: your point about guessing was right in a way I only saw with
    real data. Taking the segment before the first comma groups 19 of our 113
    rooms under "1098 XG", because that is their entire stored address. It now
    skips postal codes, so those land under "Amsterdam" — better, still a guess.
    I would like to take you up on BUILDING_NAME in IRoomMetadata; our own
    room backend holds the building as a separate field and has nowhere to put
    it. I will open that as an issue on server first so it can be discussed
    before any code.

🤖 AI (if applicable)

  • The content of this comment was partly or fully generated using AI

@Rikdekker

Copy link
Copy Markdown
Contributor Author

The failing php/psalm checks are unrelated to this PR — it changes no PHP at
all. They run against server master and fail on getBackends() mocks in tests
this branch does not touch; #8589 had the same checks green on 11 August.

Rikdekker added a commit to nextcloud/RoomVox that referenced this pull request Aug 13, 2026
Addresses are stored as a fixed four part format
"Building, Street, PostalCode, City" so the room editor can split them
back apart. That is an internal storage detail, but it was published to
CalDAV clients verbatim, so a room imported without a building or street
reached Nextcloud Calendar, Outlook and Apple Calendar as
", , 1098 XG, Amsterdam".

Empty positions are now dropped at the publishing boundary, in both the
building address and the room description. Storage is untouched, so the
editor keeps its four separate fields.

This gets more visible with the room browser in
nextcloud/calendar#8264, which groups rooms by the first segment of the
building address: without this, 95 of the 113 rooms on our test instance
group under a postal code.

Adds tests/Unit/Connector/RoomMetadataTest.php, which also pins down that
the floor is published as room-building-story and not under the
non-standard room-building-floor key it used before 1.2.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rikdekker

Rikdekker commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Some context on the building-name heuristic that may be useful for reviewing it — it does not change the code here.

The heuristic stays as agreed. Worth knowing though that it is a fallback rather than the only route: room backends can supply a building name directly. RoomVox 1.3.0 (released today) now publishes {http://nextcloud.com/ns}room-building-name from its own building field instead of leaving clients to infer one from the address, and it also fixes the floor, which it previously published under a key no client requests (room-building-floor rather than IRoomMetadata::BUILDING_STORY). On our test instance 97 of 113 rooms had a floor set that no client could see.

So for rooms from a backend that fills those properties, grouping and floor filtering run on real data; for everything else the heuristic here keeps doing its job. That is why skipping postal-code segments still matters — it is exactly the case where a backend gives us nothing better to work with.

There is no IRoomMetadata::BUILDING_NAME constant on the server side yet, so the key is currently published as a plain string; clients that do not know it simply ignore it. Adding it upstream would be the natural next step, but that is out of scope here.

Separately, @osm-frasch above mentioned evaluating RoomVox and not being able to patch Calendar in their Enterprise environment — 1.3.0 is on the App Store now, so the metadata half of what they need no longer requires a patch. The room browser in this PR is the other half.

@odzhychko

odzhychko commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

@Rikdekker Thx for reconciling the mockup and factoring out (incl. testing) filtering.
I'm taking over cleaning (code organization, some UI stuff) to bringing into main in #8731

@odzhychko odzhychko closed this Aug 15, 2026
@github-project-automation github-project-automation Bot moved this from 🏗️ In progress to ☑️ Done in 💌 📅 👥 Groupware team Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: ☑️ Done

Development

Successfully merging this pull request may close these issues.

7 participants